-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
POC: Allow hierarchical filtering of logger namespaces #22859
Conversation
Doing any kind of looping in If this is really needed, will require a well optimized algorithm, and performance impact testing in various "spammy" scenario (startup, OTA, MQTT debug, etc). |
Most of those calls are using the same few namespaces, so after the first hits, the cost is the same as currently, 1 key lookup per call. I don't believe the number of entries in the cache would add any measurable cost to the lookup I don't have the means of doing perf testing but even with that crude fallback when a new namespace is encoutered, I did not notice any slowdowns on my lowy setup. Right now the debug regex approach would need to be implemented in info level to allow spliting mqtt connection/login info messages with the flood of mqtt updates ... It would be interesting to compare the regex match cost, that is incured on every log call this time. We have really nice new hierarchical namespace, we might as well use it 😋 |
The cost once the cache is built would not be the problem. Actually, once it's built, it's probably a tiny bit faster. |
Ran some quick & dirty benchmarks for reference, simple ones on just the differences in logic involved (also threw in regex one for good measure since it's adjacent to this). Current:
Looping (cache built once):
Current / Regex:
Current with object assign cost (just to compare below rebuild without this cost / not significant):
Looping with rebuild:
Codeconst LOG_LEVELS: readonly string[] = ['error', 'warning', 'info', 'debug'] as const;
type LogLevel = typeof LOG_LEVELS[number];
const LEVEL = 'info';
const namespaces: string[] = [
'zh:zstack:unpi:parser',
'zh:zstack:unpi',
'zh:zspec',
'z2m',
];
let logger1NamespacedLevels: {[ns: string]: string} = {
'zh:zstack:unpi:parser': 'warning',
'zh:zstack:unpi': 'warning',
};
let logger2NamespacedLevels: {[ns: string]: string} = {
'zh:zstack:unpi': 'warning',
};
const log1 = (level: LogLevel, message: string, namespace: string, picked: string[]): void => {
const nsLevel = logger1NamespacedLevels[namespace] ?? LEVEL;
if (LOG_LEVELS.indexOf(level) <= LOG_LEVELS.indexOf(nsLevel)) {
picked.push(message);
}
};
const assignLevel = (namespace: string): string => {
let ns = namespace;
while (true) {
const sep = ns.lastIndexOf(":");
if (sep === -1) {
return logger2NamespacedLevels[namespace] = LEVEL;
}
ns = ns.slice(0, sep);
if (logger2NamespacedLevels[ns]) {
return logger2NamespacedLevels[namespace] = logger2NamespacedLevels[ns];
}
}
};
const log2 = (level: LogLevel, message: string, namespace: string, picked: string[]): void => {
const nsLevel = logger2NamespacedLevels[namespace] ?? assignLevel(namespace);
if (LOG_LEVELS.indexOf(level) <= LOG_LEVELS.indexOf(nsLevel)) {
picked.push(message);
}
};
const log3DebugNamespaceIgnoreRegex = new RegExp(/^zh:zstack:unpi.*/);
const log3 = (level: LogLevel, message: string, namespace: string, picked: string[]): void => {
if (log3DebugNamespaceIgnoreRegex?.test(namespace)) {
return;
}
const nsLevel = logger2NamespacedLevels[namespace] ?? LEVEL;
if (LOG_LEVELS.indexOf(level) <= LOG_LEVELS.indexOf(nsLevel)) {
picked.push(message);
}
};
console.log(namespaces);
console.log(JSON.stringify(logger1NamespacedLevels));
console.log(JSON.stringify(logger2NamespacedLevels));
console.log(log3DebugNamespaceIgnoreRegex);
let current = 0;
let looping = 0;
let regex = 0;
let currentRebuild = 0;
let loopingRebuild = 0;
const benchCurrent = (): void => {
const picked: string[] = [];
log1('info', 'Test1-0', namespaces[0], picked);
log1('info', 'Test1-1', namespaces[1], picked);
log1('info', 'Test1-2', namespaces[2], picked);
log1('info', 'Test1-3', namespaces[3], picked);
current += picked.length;
};
const benchLooping = (): void => {
const picked: string[] = [];
log2('info', 'Test2-0', namespaces[0], picked);
log2('info', 'Test2-1', namespaces[1], picked);
log2('info', 'Test2-2', namespaces[2], picked);
log2('info', 'Test2-3', namespaces[3], picked);
looping += picked.length;
};
const benchRegex = (): void => {
const picked: string[] = [];
log3('info', 'Test3-0', namespaces[0], picked);
log3('info', 'Test3-1', namespaces[1], picked);
log3('info', 'Test3-2', namespaces[2], picked);
log3('info', 'Test3-3', namespaces[3], picked);
regex += picked.length;
};
const benchCurrentRebuild = (): void => {
logger1NamespacedLevels = {
'zh:zstack:unpi:parser': 'warning',
'zh:zstack:unpi': 'warning',
};
const picked: string[] = [];
log1('info', 'Test10-0', namespaces[0], picked);
log1('info', 'Test10-1', namespaces[1], picked);
log1('info', 'Test10-2', namespaces[2], picked);
log1('info', 'Test10-3', namespaces[3], picked);
currentRebuild += picked.length;
};
const benchLoopingRebuild = (): void => {
logger2NamespacedLevels = {
'zh:zstack:unpi': 'warning',
};
const picked: string[] = [];
log2('info', 'Test20-0', namespaces[0], picked);
log2('info', 'Test20-1', namespaces[1], picked);
log2('info', 'Test20-2', namespaces[2], picked);
log2('info', 'Test20-3', namespaces[3], picked);
loopingRebuild += picked.length;
};
// for (const bench of [benchCurrent, benchLooping, benchRegex, benchCurrentRebuild, benchLoopingRebuild]) {
// console.time(bench.name);
// for (let i = 0; i < 1000000; i++) {
// bench();
// }
// console.timeEnd(bench.name);
// }
// console.log(current, looping, regex, currentRebuild, loopingRebuild);
import Benchmark from 'benchmark';
const suite = new Benchmark.Suite();
suite.add('Current', benchCurrent);
suite.add('Looping', benchLooping);
suite.add('Regex', benchRegex);
suite.add('Current with object assign cost', benchCurrentRebuild);
suite.add('Looping with rebuild', benchLoopingRebuild);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
suite.on('cycle', function(event: any) {
console.log(String(event.target));
});
suite.on('complete', function() {
console.log(JSON.stringify(logger1NamespacedLevels));
console.log(JSON.stringify(logger2NamespacedLevels));
console.log(current, looping, regex, currentRebuild, loopingRebuild);
// @ts-expect-error bench
console.log(this.filter('successful').map((b) => b.count));
});
suite.run({'async': true}); |
That last commit forces the use of the loop, definitely worse for performance. |
Thank you for taking the time to bechmark test this out... we really don't have the same definition of "quick and dirty" 😅 The cache build/rebuild situation would happen only at startup and level config change, and only on the first occurence of a namespace... My sample of 1 is my current setup: a small /medium network of ~60 devices on a pi 3, that also hosts a small HA and a couple of other services ... not the beefiest setup but so far so good |
It might be better to use the cache indeed 😄 |
The spread operator is a no-go here too I think. It always has terrible performance for some reason.
It doesn't fallback ( This is likely to be a problem if triggered (a level is changed, cache needs rebuilding) while Z2M is already having a hard time keeping up with inc/out (during an OTA or on a network with a bunch of terrible spammy Tuya... the usual troublemakers), since this will affect lower levels the most, and serial comm is the most chatty. |
I'm probably missing some context... Even then from my understanding, during a heavy OTA, the namespaces used will just repeat and hit the cache, only the first handful of serial /ota /whatever tuya will get the namespace search hit... and the rest will be very slightly sped up... And as you said the benchmark is only of the filtering, when adding the full transport overhead, i'm assuming the difference would even be less... I do have a bunch of the chatty tuya power consumption stuff, but no OTA ready to test this... On my side, I want to silence namespaces branches without having to go hunt for each new subnamespace 😬 Is anyone with a big network and some devices to update on a very ressource constrained machine willing to do some tests 😅 ? |
Have you tried changing a level during a time of chatty behavior (or maybe try changing the advanced:
log_level: info
log_namespaced_levels:
zh: debug
zhc: debug You're right though, the cost of the extra logic is probably drowned in the relative expensiveness of the logging itself, the difference won't be less, but hidden because of the drastically lower ops/sec. |
With the commit I just added, it looks like this. Since I changed benchmark tool, I figured I should post the previous stuff back for reference.
(That darn spread operator 😓) Now the actual logging (exact same logger class as Z2M, just made to stand alone), 'console' only as output ('file' introduces too much randomness). Bench with 5s runtime, 1s warmup. Each run calls
|
I didn't go as far as script log level changes requests in a fast manner, but with "quick" manual changes, I didn't see any adverse effect... |
@Koenkk Can you see any usage issue with this automatic hierarchical filtering? It would prevent setting only an upper namespace to a specific level, without affecting below namespaces (would require manually setting the below namespaces, basically reverses current behavior). |
@Nerivec I don't see any issues with that, PR also looks OK so let me know if this can be merged. Edit: only thing left is a PR for a docs update. |
|
b6fef7e
to
41fb6f3
Compare
matches the closes defined namespace log level: advanced: log_namespaced_levels: 'zh:zstack:unpi': info would also apply to zh:zstack:unpi:write and zh:zstack:unpi:parse To limit runtime penalty uses the namespacedLevels as a lazy cache
Avoid poluting the namespacedLevel in the config / UI Add cache reset on config time log level change
41fb6f3
to
803c76b
Compare
Fix for #22935 will use winston hotpath and avoid splat logic entirely ( |
Thanks! |
* mention the hierarchical namespace for logging Documents Koenkk/zigbee2mqtt#22859 * fix wording according to review
* POC: Allow hierarchical filtering of logger namespaces matches the closes defined namespace log level: advanced: log_namespaced_levels: 'zh:zstack:unpi': info would also apply to zh:zstack:unpi:write and zh:zstack:unpi:parse To limit runtime penalty uses the namespacedLevels as a lazy cache * Move namespace levels to separate cache Avoid poluting the namespacedLevel in the config / UI Add cache reset on config time log level change * Actually use the cache * Optimize/Cleanup * Add some namespace hierarchy filter tests * More coverage. * Fix Koenkk#22935 --------- Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
@@ -102,9 +106,6 @@ class Logger { | |||
|
|||
const options: KeyValue = { | |||
app_name: 'Zigbee2MQTT', | |||
format: winston.format.printf(/* istanbul ignore next */(info) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this removed? I think this causes #23241
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems it might not output the message without expressly returning it with printf?
format: winston.format.printf((info) => info.message),
Strange that it's undefined
though, I would think it would be the stringified json of info
param as default if not message
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ghoz could you comment on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could, but I would not have much of interest to say ...
I see it has been removed when going for the winston hotpath directly
827e18e#diff-be38649fab0f205ab799e4de3d3b56ad7d54397f525e8f370cda402efcece524L109
but I'm not familiar with this low level to be honest...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a syslog enabled system to test it out, but I'd suggest reverting this hunk...
@Nerivec do you remember why it was removed ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed. It works using:
const options: KeyValue = {
app_name: 'Zigbee2MQTT',
format: winston.format.printf((info) => info.message),
...settings.get().advanced.log_syslog,
};
Not sure why it doesn't output json by default (might be the combine
during logger init), but since we don't want json anyway, definitely a bit too much "cleanup" in my last commit here 😅
We might want to add a test to cover this codepath (would have to hijack way above syslog though I guess).
When playing with the new
log_namespaced_levels
#22619, I expected that'zh:zstack:unpi' :info
would match all namespaces below, such aszh:zstack:unpi:parser
, writer, etcThis PR implements a quick POC to allow that.
To limit runtime penalty it uses
the samea lazy cache: the first time a new namespace is seen, it tries to find if any namespace above is defined in the config and stores the level, So in effectnamespacedLevels
asWould also apply to
zh:zstack:unpi:writer
,zh:zstack:unpi:parser
and anything else belowzh:zstack:unpi
I might not have seen yet. Of course if a more specific config is defined, it would be picked up.This would allow to add new namespace sublevels without breaking existing configuration, for example
z2m:mqtt:connexion
andz2m:mqtt:publish
BUGS / TODOs :